--[[
This module implements a closed channel for asynchronous communication using Signal and Listener 
primitives. Users are not required to use the primitive classes directly. Instead, the listen() 
and emit() methods of the Channel class should be used. For example:

    component_A.lua
        function bar(signal)
            print(signal)
        end
        channel = Channel:new()
        channel:listen("foo", bar)  -- wait until a matching signal is heard
        
... and later, possibly by another component ignorant of the listener

    component_B.lua
        channel:emit("foo")  -- A's listener will be activated now and print the signal
    
A hierarchy of channels can be set up by linking channels through their 'parent' attribute. 
Whenever a signal is emitted in a channel, after activating the local listeners, the channel emits 
the same signal on the parent channel (if a parent is available).
--]]
module(..., package.seeall)

import("utils.node", "Node")
local str_fmt = string.format


Channel = class("Channel", Node)
Channel.__tostring = Node.__tostring  -- because metamethods are not inherited...

function Channel.__init(self, name, validation)
    Node.__init(self, name)  -- nodes can have one parent and multiple children
    self.validation = (validation == nil) and true or validation  -- type validation flag
    self.registered = set()  -- registered signal types (for signal and listener validation)
    self.listeners = {}      -- list of lists of active (deployed) listeners
    self.stack = list()      -- contains the currently executing signals and listeners
end

function Channel.set_log(self, enabled)
    --[[ This method enables or disables logging of signal broadcasts and listener activations in 
    this channel. ]]
    enabled = enabled or false
    if enabled then
        self.stack.append = function(s, o)
            list.append(s, o)
            local indent = ("    "):rep(#s - 1)
            io.write(str_fmt("[%20s] %d) %s%s\n", self.fullname, #s, indent, tostring(o)))
        end
    else
        self.stack.append = nil
    end
end

function Channel.register(self, ...)
    --[[ Register new signal types in the channel. Channels will not accept signals or listeners 
    of a given type before it is registered. ]]
    for _, signal_type in ipairs{...} do
        self.registered:add(signal_type)
    end
end

function Channel.unregister(self, ...)
    --[[ Remove (if present) the given signal types from the channel. ]]
    for _, signal_type in ipairs{...} do
        self.registered:discard(signal_type)
    end
end

function Channel.listen(self, signal_type, callback, priority, id)
    --[[ Creates a Listener object and attaches it to the channel. Returns the new listener. ]]
    local listener = Listener(self, signal_type, callback, priority, id)
    listener:start()
    return listener
end

function Channel.emit(self, signal_type, payload)
    --[[ Creates a Signal object and broadcasts it on the channel. Returns the new signal. ]]
    local signal = Signal(self, signal_type, payload)
    signal:emit()
    return signal
end

function Channel._insert(self, listener)
    --[[ Add a listener to this channel. This method is meant to be private. Use Listener.start() 
    instead. Also note that a listener is automatically inserted into the channel by the 
    Channel.listen() method. ]]
    if self.validation and listener.type ~= "*" and 
    not self.registered:contains(listener.type) then
        error(str_fmt("unable to listen signal type '%s' (not registered)", listener.type))
    end
    local listeners = self.listeners[listener.type]
    if listeners == nil then
        self.listeners[listener.type] = list(listener)
    else
        local priorities = listeners:map(function(l) return l.priority end)
        local index = priorities:bisect_right(listener.priority)
        listeners:insert(index, listener)
    end
end

function Channel._remove(self, listener)
    --[[ Remove a listener from the channel. This method is meant to be private. Instead of 
    directly calling this method, use Listener.stop(). Note that a listener will not be 
    automatically stopped, so unless you want a listener to run forever you *must* call 
    Listener.stop(). ]]
    local listeners = self.listeners[listener.type]
    if #listeners == 1 then
        assert(listener == listeners[1])
        self.listeners[listener.type] = nil
    else
        listeners:remove(listener)
    end
end

function Channel._broadcast(self, signal)
    --[[ Find and activate all listeners attached to this channel which match 'signal'. ]]
    if self.validation and not self.registered:contains(signal.type) then
        error(str_fmt("unable to emit signal type '%s' (not registered)", signal.type))
    end
    local stack = self.stack
    stack:append(signal)
    for __, signal_type in ipairs{signal.type, "*"} do
        local listeners = self.listeners[signal_type]
        if listeners ~= nil then
            for _, listener in ipairs(listeners:copy()) do
                if listener.deployed then
                    stack:append(listener)
                    listener:_activate(signal)
                    stack:pop()
                end
            end
        end
    end
    if self.parent ~= nil then
        self.parent:_broadcast(signal)
    end
    stack:pop()
end


----------------------------------------------------------------------------------------------------
-- Listener
----------------------------------------------------------------------------------------------------
--[[ Implements a wait for a signal. Whenever some object broadcasts a signal with the same type on
the same channel, the listener will activate. If the type is nil or "*", the listener will be 
activated by the first signal on the channel, independently of the signal's type.
Listeners also have a 'priority' attribute that defines the order by which listeners are 
activated when a signal triggers multiple listeners simultaneously (smaller priority activate 
first). Listeners with the same priority activate by the order of their insertion into the 
channel (when the start() method is called). ]]
Listener = class("Listener")

function Listener.__init(self, channel, signal_type, callback, priority, id)
    if callback == nil then
        error("listener callback cannot be nil", 2)
    end
    self.channel = channel          -- Channel - the channel where the listener is deployed
    self.type = signal_type or "*"  -- string - type of signal that triggers the listener
    self.callback = callback        -- callable - function called when the listener is triggered
    self.priority = priority or 0   -- number - listener activation priority
    self.id = id                    -- object - something to identify the listener's origin
    self.match = nil                -- Signal - signal object that triggered the listener
    self.deployed = false           -- boolean - indicates whether the listener is active or not
end

function Listener.__tostring(self)
    return str_fmt("<%s '%s' (id=%s, prio=%f)>", self.__name, 
                   self.type, tostring(self.id), self.priority)
end

function Listener.start(self)
    if not self.deployed then
        self.deployed = true
        self.channel:_insert(self)
    end
end

function Listener.stop(self)
    if self.deployed then
        self.deployed = false
        self.channel:_remove(self)
    end
end

function Listener._activate(self, signal)
    self.match = signal
    self.callback(self, signal)
end

----------------------------------------------------------------------------------------------------
-- Signal
----------------------------------------------------------------------------------------------------
--[[ Signals are an instantaneous primitive that allows synchronizing several components 
listening on the same channel. Signals activate listeners, which wait until a signal of 
matching type is emitted on the same channel. ]]
Signal = class("Signal")

function Signal.__init(self, channel, signal_type, payload)
    if signal_type == nil then
        error("signal type cannot be nil", 2)
    elseif signal_type == "*" then
        error("cannot use wildcard '*' as signal type", 2)
    end
    self.channel = channel
    self.type = signal_type
    self.payload = payload
end

function Signal.__tostring(self)
    local payload = (self.payload ~= nil) and (" " .. table.repr(self.payload)) or ""
    return str_fmt("<%s '%s'%s>", self.__name, self.type, payload)
end

function Signal.emit(self, new_payload)
    if new_payload ~= nil then
        self.payload = table.copy(new_payload, self.payload)
    end
    self.channel:_broadcast(self)
end


----------------------------------------------------------------------------------------------------
-- Test code
----------------------------------------------------------------------------------------------------
function test()
    require "utils.rng"
    
    local c = Channel("SIC")
    c:set_log(true)
    c:register("foo", "bar")
    
    local dummy = function() end
    for i = 1, 10 do
        c:listen("foo", dummy, rng.uniform(-1, 1), i)
        c:listen("*", dummy, rng.uniform(0, 2), i)
    end
    
    c:emit("foo", {x=0, y=4})
    c:emit("bar")
    
    local success, message = pcall(c.emit, c, "baz")
    if not success then
        print(message)
        print("turning off channel validation...")
        c.validation = false
        c:emit("baz")
    end
    table.pprint(c.listeners, 1)
    return c
end
